package main

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/logger"
	"github.com/gofiber/fiber/v3/middleware/static"
	"github.com/gofiber/template/html/v3"

	"github.com/redis/go-redis/v9"
)

// Some const
const (
	recaptchaURL = "https://recaptcha.net/recaptcha/api/siteverify"
)

// RecaptchaResponse is the struct of json recv from recaptcha.net
type RecaptchaResponse struct {
	Success     bool      `json:"success"`
	ChallengeTs time.Time `json:"challenge_ts"`
	Hostname    string    `json:"hostname"`
	Score       float64   `json:"score"`
	Action      string    `json:"action"`
}

// Info read from cli
var (
	runAddress      = flag.String("address", "0.0.0.0", "Pastebin Listen Port")
	runPort         = flag.String("port", "80", "Pastebin Bind IP")
	useRecaptcha    = flag.Bool("userecaptcha", false, "Use Google Recaptcha or not")
	recaptchaSecret = flag.String("secretkey", "", "Recaptcha Secret Key")
	recaptchaPublic = flag.String("publickey", "", "Recaptcha site key")
	recaptchaScore  = flag.Float64("recaptcharate", 0.6, "Recaptcha verify score")
	redisAddress    = flag.String("redisadd", "127.0.0.1", "RedisIP")
	redisPort       = flag.String("redisport", "6379", "RedisPort")
)

// Create Global var
var (
	app *fiber.App
	rdb *redis.Client
	appIp string
	rdbIp string
	ctx context.Context
	err error
)

func init() {
	flag.Parse()

	// IP address + port
	appIp = *runAddress + ":" + *runPort
	rdbIp = *redisAddress + ":" + *redisPort

	// Create redis client
	rdb = redis.NewClient(&redis.Options{
		Addr:     rdbIp,
		Password: "", // no password set
		DB:       0,  // use default DB
	})

	// Test redis connection
	ctx = context.Background()
	_, err = rdb.Ping(ctx).Result()
	if err != nil {
		fmt.Println("Can't not connect to redis: ", err)
		os.Exit(2)
	}

	// Create template engine
	engine := html.New("./public", ".html")

	// Create fiber app
	app = fiber.New(fiber.Config{
		Views: engine,
	})

	// Add logger middleware
	app.Use(logger.New())

	// Static assets Handler
	app.Use("/css", static.New("./public/css"))
	app.Use("/js", static.New("./public/js"))
	app.Use("/img", static.New("./public/img"))
}

func main() {
	// Method: GET
	// Main Webpage
	app.Get("/", mainPageHandler)

	// Method: POST
	// Recv User input
	app.Post("/paste", inputPageHandler)

	// Method: GET
	// Show Paste data
	app.Get("/:id", pasteDataHandler)

	// Method: GET
	// Show RAW data
	app.Get("/raw/:id", rawDataHandler)

	// Start server
	log.Printf("Now listening on: http://%s", appIp)
	log.Printf("Application started. Press CTRL+C to shut down")

	err := app.Listen(appIp)
	if err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

// Method: GET
// Main Webpage
func mainPageHandler(c fiber.Ctx) error {
	if *useRecaptcha {
		return c.Render("input", fiber.Map{
			"recaptchaPublic": *recaptchaPublic,
		})
	} else {
		return c.Render("input_no_recaptcha", nil)
	}
}

// Method: POST
// Recv User input
func inputPageHandler(c fiber.Ctx) error {
	// Verify with recaptcha
	if !verify(c) {
		return c.Render("error", nil)
	}

	text := c.FormValue("text")
	if len(text) > 81920 {
		return c.Status(fiber.StatusBadRequest).SendString("Text too long")
	}

	// check duration valid
	duration := c.FormValue("duration")
	expire, err := strconv.Atoi(duration)
	if err != nil || expire < 0 || expire > 18000 {
		return c.Status(fiber.StatusBadRequest).SendString("Invalid duration")
	}

	// Generate an ID with md5[0:6]
	textMd5 := md5.New()
	io.WriteString(textMd5, text)
	textID := (hex.EncodeToString(textMd5.Sum(nil)))[0:6]

	log.Printf("IP:%s Send a paste %s", c.IP(), textID)

	err = rdb.Set(ctx, textID, text, time.Duration(expire)*time.Second).Err()
	if err != nil {
		log.Printf("Failed to save paste to Redis: %v", err)
		return c.Status(fiber.StatusInternalServerError).SendString("Failed to save paste")
	}

	return c.Redirect().Status(fiber.StatusFound).To("/"+textID)
}

// Method: GET
// Show Paste data
func pasteDataHandler(c fiber.Ctx) error {
	textID := c.Params("id")

	v, err := rdb.Get(ctx, strings.ToLower(textID)).Result()
	if err == redis.Nil {
		// Key not exist
		return c.Redirect().Status(fiber.StatusFound).To("/")
	} else if err != nil {
		// other error
		log.Printf("Redis error: %v", err)
		return c.Redirect().Status(fiber.StatusFound).To("/")
	} else {
		domain := c.BaseURL()
		if !strings.HasSuffix(domain, "/") {
			domain += "/"
		}
		return c.Render("result", fiber.Map{
			"id":      textID,
			"content": v,
			"domain":  domain,
		})
	}
}

// Method: GET
// Show RAW data
func rawDataHandler(c fiber.Ctx) error {
	textID := c.Params("id")

	v, err := rdb.Get(ctx, strings.ToLower(textID)).Result()
	if err == redis.Nil {
		return c.Status(fiber.StatusNotFound).SendString("Paste not found")
	} else if err != nil {
		log.Printf("Redis error: %v", err)
		return c.Status(fiber.StatusInternalServerError).SendString("Internal server error")
	} else {
		// Set content type to plain text
		c.Set("Content-Type", "text/plain; charset=utf-8")
		return c.SendString(v)
	}
}

// Verify reCAPTCHA
func verify(c fiber.Ctx) bool {
	if !*useRecaptcha {
		return true
	}

	// Makeup URL
	verifyURL, _ := url.Parse(recaptchaURL)
	arg := verifyURL.Query()
	arg.Set("secret", *recaptchaSecret)
	arg.Set("response", c.FormValue("g-recaptcha-response"))
	verifyURL.RawQuery = arg.Encode()

	// Send to recaptcha verify server
	recv, err := http.Get(verifyURL.String())
	if err != nil {
		log.Printf("Can't connect to recaptcha server: %v", err)
		return false
	}
	defer recv.Body.Close()

	// Get json
	result, err := io.ReadAll(recv.Body)
	if err != nil {
		log.Printf("Failed to read recaptcha response: %v", err)
		return false
	}

	// Unmarshal Json to Struct
	var reRes RecaptchaResponse
	err = json.Unmarshal(result, &reRes)
	if err != nil {
		log.Printf("Failed to parse recaptcha response: %v", err)
		return false
	}

	// If verify succeeded and user score >= recaptchaScore then return true
	if reRes.Success && reRes.Score >= *recaptchaScore {
		return true
	}
	return false
}
